package code.google.jcustomize;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.jsp.el.ELException;
import javax.servlet.jsp.el.ExpressionEvaluator;
import javax.servlet.jsp.el.FunctionMapper;
import javax.servlet.jsp.el.VariableResolver;

import org.apache.commons.el.ExpressionEvaluatorImpl;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

public class Setup {
    private final static String OPT_DEFAULTS        = "--defaults";
    private final static String OPT_CONFIG          = "--config";
	
    private Map opts;
    private Map variables;
    private Properties props;
    private Document configDocument;
    private ConfigSource configSource;
    private List config; 
    private List outputs;
    private List actions;
    
    Setup() {
        opts = new HashMap();
        variables = new HashMap();
    }
    
    private void fillOpts(String[] args) {
        for (int i = 0; i < args.length - 1; i++) {
            final String opt = args[i];
            if (opt.startsWith("--")) opts.put(opt.toLowerCase(), args[++i]);
        }
    }
    
    private String getDefaultsPath() {
        String result = (String)opts.get(OPT_DEFAULTS);
        if (result == null) help();
        return result;
    }
    
    private String getConfigPath() {
        String result = (String)opts.get(OPT_CONFIG);
        if (result == null) help();
        return result;
    }
    
    private void help() {
        System.out.println("Usage: "+this.getClass().getName()+" "+OPT_DEFAULTS+" <path> "+OPT_CONFIG+" <path>");
        System.exit(1);
    }

    private void loadDefaults() {
        String path = getDefaultsPath();
        props = new Properties();
        InputStream is = null;
        try {
            is = new FileInputStream(path);
            props.load(is);
            for (Enumeration e = props.keys(); e.hasMoreElements(); ) {
                String key = (String)e.nextElement();
                variables.put(key, props.get(key));
            }
        } catch (IOException ioe) {
            System.out.println(ioe.getMessage());
        } finally {
            if (is != null) try { is.close(); } catch (IOException ioe) {}
        }
    }
    
    public static String el_IfThenElse(boolean condition, String case1, String case2) {
        return (!condition) ? case2 : case1;
    }
    
    public static String el_RegExTract(String str, String regex, long group) {
        if (str == null || regex == null)
            return null;
        try {
            Pattern pattern = Pattern.compile(regex);
            Matcher matcher = pattern.matcher(str);
            if (matcher.matches()) {
                return matcher.group((int)group);
            } else {
                return "";
            }
        } catch (Throwable t) {
            t.printStackTrace();
            System.out.println("String is \""+str+"\"");
            System.out.println("Pattern is \""+regex+"\"");
            return null;
        }
    }
    
    public static String el_AddSeparator(String path) {
        if (path == null || path.length() == 0) return "";
        if (path.endsWith(File.separator)) return path;
        if (path.charAt(path.length()-1) == '/') return path;
        return path + File.separator;
    }
    
    public static String el_DelSeparator(String path) {
        if (path == null || path.length() == 0) return "";
        if (!(path.endsWith(File.separator) || path.charAt(path.length()-1) == '/')) return path;
        return path.substring(0, path.length()-1);
    }

    public static String el_CONCAT(String s1, String s2) {
        if (s1 == null && s2 == null) return null;
        if (s1 == null) return s2;
        if (s2 == null) return s1;
        return s1 + s2;
    }
    
    public static String el_ESCAPE(String path) {
        if (path == null || path.length() == 0) return "";
        StringBuffer sb = new StringBuffer(path.length()*2+1);
        for (int i = 0; i < path.length(); i++) {
            final char c = path.charAt(i);
            if (c == '\\') sb.append("\\\\"); else sb.append(c);
        }
        return sb.toString();
    }
    
    private final static FunctionMapper FM = new FunctionMapper() {
        private final Method ifthenelse;
        private final Method regextract;
        private final Method addpathsep;
        private final Method delpathsep;
        private final Method escape;
        private final Method concat;
        
        {
            Method m = null;
            try {
                m = Setup.class.getDeclaredMethod("el_IfThenElse", new Class[] {boolean.class, String.class, String.class});
            } catch (NoSuchMethodException nsme) {
            }
            ifthenelse = m;
            try {
                m = Setup.class.getDeclaredMethod("el_RegExTract", new Class[] {String.class, String.class, long.class});
            } catch (NoSuchMethodException nsme) {
                m = null;
            }
            regextract = m;
            try {
                m = Setup.class.getDeclaredMethod("el_AddSeparator", new Class[] {String.class});
            } catch (NoSuchMethodException nsme) {
                m = null;
            }
            addpathsep = m;
            try {
                m = Setup.class.getDeclaredMethod("el_DelSeparator", new Class[] {String.class});
            } catch (NoSuchMethodException nsme) {
                m = null;
            }
            delpathsep = m;
            try {
                m = Setup.class.getDeclaredMethod("el_ESCAPE", new Class[] {String.class});
            } catch (NoSuchMethodException nsme) {
                m = null;
            }
            escape = m;
            try {
                m = Setup.class.getDeclaredMethod("el_CONCAT", new Class[] {String.class, String.class});
            } catch (NoSuchMethodException nsme) {
                m = null;
            }
            concat = m;
        }
        
        public Method resolveFunction(String prefix, String name) {
            if ("ifthenelse".equalsIgnoreCase(name)) {
                return ifthenelse;
            } 
            else if ("regextract".equalsIgnoreCase(name)) {
                return regextract;
            }
            else if ("addseparator".equalsIgnoreCase(name)) {
                return addpathsep;
            }
            else if ("delseparator".equalsIgnoreCase(name)) {
                return delpathsep;
            }
            else if ("escape".equalsIgnoreCase(name)) {
                return escape;
            }
            else if ("concat".equalsIgnoreCase(name)) {
                return concat;
            }
            return null;
        }
    };
    
    private final VariableResolver VR = new VariableResolver() {
        public Object resolveVariable(String var) throws ELException {
            String val = (String)variables.get(var);
            if (val != null && val.indexOf("${") >= 0) {
                ExpressionEvaluator ee = new ExpressionEvaluatorImpl();
                String newVal = (String)ee.evaluate(val, String.class, this, FM);
                variables.put(var, val = newVal);
            }
            return val;
        }
    };
    
    private void loadConfig() {
        InputStream is = null;
        try {
            is = new FileInputStream(getConfigPath());
            configSource = new ConfigSource();
            configDocument = configSource.readDocument(is);
            config = new ArrayList();
            outputs = new ArrayList();
            actions = new ArrayList();
            Node root = configDocument.getDocumentElement();
            if (root != null)
                for (Node n = root.getFirstChild(); n != null; n = n.getNextSibling()) {
                    if (n.getNodeType() != Node.ELEMENT_NODE) continue;
                    final String name = n.getNodeName();
                    if ("config".equalsIgnoreCase(name)) {
                        try {
                            AbstractConfig cfg = AbstractConfig.read(n);
                            if (cfg != null) config.add(cfg);
                        } catch (ClassNotFoundException e) {
                            System.out.println(e.getMessage());
                        }
                    } 
                    else if ("output".equalsIgnoreCase(name)) {
                        ResultSerializer rs = new ResultSerializer();
                        rs.parse((Element)n);
                        outputs.add(rs);
                    }
                    else if ("actions".equalsIgnoreCase(name)) {
                        for (Node child = n.getFirstChild(); child != null; child = child.getNextSibling()) {
                            if (child.getNodeType() != Node.ELEMENT_NODE)
                                continue;
                            AbstractAction action = AbstractAction.read(child);
                            if (action != null) actions.add(action);
                        }
                    }

                }
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } finally {
            if (is != null) try { is.close(); } catch (IOException ioe) {};
        }
    }
    
    private boolean makePatches() {
        boolean ok = true;
        
        for (Iterator it = config.iterator(); it.hasNext(); ) {
            AbstractConfig cfg = (AbstractConfig)it.next();
            if (cfg.skip(VR, FM)) continue;
            String processing = "Processing config {0}...";
            if (processing != null) System.out.println(MessageFormat.format(processing, new Object[] {cfg.getName()}));
            ok = ok && cfg.patch(configSource, VR, FM);
        }
        
        for (Iterator it = outputs.iterator(); it.hasNext(); ) {
            ResultSerializer rs = (ResultSerializer)it.next();
            ok = ok && rs.write(configSource, VR, FM);
        }
        
        ExpressionEvaluator ee = new ExpressionEvaluatorImpl();
        for (Iterator it = actions.iterator(); it.hasNext(); ) {
            AbstractAction action = (AbstractAction)it.next();
            if (action.skip(configSource, ee, VR, FM)) continue;
            ok = ok && action.perform(configSource, ee, VR, FM);
        }
        
        String result = ok ? "Done." : "Done with some errors";
        if (result != null) System.out.println(result);
        return ok;
    }
    
    private void done() {
    }
    
    public static void main(String[] args) {
        Setup setup = new Setup();
        setup.fillOpts(args);
        setup.loadDefaults();
        setup.loadConfig();
        setup.makePatches();
        setup.done();
    }

}
